﻿using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using SmartCA.Infrastructure.DomainBase;
using SmartCA.Infrastructure.RepositoryFramework;
using SmartCA.Model.Membership;
using System.Security;

namespace SmartCA.Infrastructure.Membership.Providers
{
    public abstract class ClientMembershipProvider
        : IClientMembershipProvider, IUnitOfWorkRepository
    {
        #region Constants

        private static class Constants
        {
            public const int SaltSizeInBytes = 0x10;
            public const string NoEncryptedPasswordsWithAutoGenKeys = 
                "Cannot use encrypted passwords with autogenerated keys";
            public const string CannotDecodeHashedPassword = 
                "Provider cannot decode hashed password";
            public const string NoPasswordRetrieval =
                "Password retrieval is not supported";
            public const int PasswordSize = 14;
        }

        #endregion

        #region Private Fields

        private IUnitOfWork unitOfWork;

        #endregion

        #region Constructors

        protected ClientMembershipProvider()
        {
            this.unitOfWork = new UnitOfWork();
            this.Initialize();
        }

        #endregion

        #region Protected Abstract Methods

        protected abstract void Initialize();
        protected abstract ClientMembershipUser GetUser(User user);
        protected abstract void PersistChangedPassword(ClientMembershipUser user, 
            string newPassword);
        protected abstract void PersistChangedPasswordQuestionAndAnswer(
            ClientMembershipUser user, string password,
            string newPasswordAnswer);
        protected abstract string GetPasswordFromPersistence(
            ClientMembershipUser user);
        protected abstract string GetPasswordAnswerFromPersistence(
            ClientMembershipUser user);
        protected abstract void PersistResetPassword(ClientMembershipUser user, 
            string encodedPasswordAnswer, string newEncodedPassword);
        protected abstract void PersistResetPassword(ClientMembershipUser user,
            string newEncodedPassword);
        protected abstract void PersistUser(ClientMembershipUser user);

        #endregion

        #region Protected Virtual Methods

        protected virtual string GetPassword(ClientMembershipUser user, 
            string passwordAnswer)
        {
            // Make sure password retrievals are allowed
            User.CheckPasswordRetrieval();

            // Validate the user's password answer
            this.ValidateUserWithPasswordAnswer(user, passwordAnswer, true);

            // Get the user's password from persistence
            return this.GetPasswordFromPersistence(user);
        }

        protected virtual string ResetPassword(ClientMembershipUser user)
        {
            User.CheckPasswordReset(null);

            string newPassword = System.Web.Security.Membership.GeneratePassword(
                      (this.Application.MinRequiredPasswordLength < Constants.PasswordSize) ?
                        Constants.PasswordSize : this.Application.MinRequiredPasswordLength,
                        this.Application.MinRequiredNonAlphanumericCharacters);

            string newEncodedPassword = this.EncodePassword(newPassword,
                user.PasswordFormat, user.PasswordSalt);

            this.PersistResetPassword(user, newEncodedPassword);

            return newPassword;
        }

        protected virtual string ResetPassword(ClientMembershipUser user, 
            string passwordAnswer)
        {
            // Are password resets allowed?
            User.CheckPasswordReset(passwordAnswer);

            // Validate the user's password answer
            this.ValidateUserWithPasswordAnswer(user, passwordAnswer, true);

            int maxPasswordSize =
                (this.Application.MinRequiredPasswordLength < 
                    Constants.PasswordSize)
                        ? Constants.PasswordSize : 
                        this.Application.MinRequiredPasswordLength;

            // Create the new password
            string newPassword = System.Web.Security.Membership.GeneratePassword(
                maxPasswordSize,
                this.Application.MinRequiredNonAlphanumericCharacters);

            // Encode the password
            string newEncodedPassword = this.EncodePassword(newPassword,
                user.PasswordFormat, user.PasswordSalt);
            
            // Save the user's new password
            this.PersistResetPassword(user, newEncodedPassword);

            // return the new password (not the encoded one!)
            return newPassword;
        }

        protected virtual void ChangePasswordQuestionAndAnswer(
            ClientMembershipUser user, string password, 
            string newPasswordQuestion, string newPasswordAnswer)
        {
            SecurityHelper.CheckParameter(newPasswordQuestion, 
                this.Application.RequiresQuestionAndAnswer,
                this.Application.RequiresQuestionAndAnswer, false,
                this.Application.MaxPasswordQuestionSize, "newPasswordQuestion");
            
            if (newPasswordAnswer != null)
            {
                newPasswordAnswer = newPasswordAnswer.Trim();
            }

            SecurityHelper.CheckParameter(newPasswordAnswer, 
                this.Application.RequiresQuestionAndAnswer,
                this.Application.RequiresQuestionAndAnswer, false,
                this.Application.MaxPasswordAnswerSize, "newPasswordAnswer");

            // Validate the user before making any changes
            this.ValidateUserWithPassword(user, password, true);

            string encodedPasswordAnswer;

            if (!string.IsNullOrEmpty(newPasswordAnswer))
            {
                encodedPasswordAnswer = this.EncodePassword(
                    newPasswordAnswer.ToLower(CultureInfo.InvariantCulture),
                    user.PasswordFormat, user.PasswordSalt);
            }
            else
            {
                encodedPasswordAnswer = newPasswordAnswer;
            }

            SecurityHelper.CheckParameter(encodedPasswordAnswer,
                this.Application.RequiresQuestionAndAnswer,
                this.Application.RequiresQuestionAndAnswer, false,
                this.Application.MaxPasswordAnswerSize, "newPasswordAnswer");

            this.PersistChangedPasswordQuestionAndAnswer(user, password,
                encodedPasswordAnswer);
        }

        protected virtual void ChangePassword(ClientMembershipUser user, 
            string oldPassword, string newPassword)
        {
            SecurityHelper.CheckParameter(oldPassword, true, true, false,
                this.Application.MaxPasswordSize, "oldPassword");
            SecurityHelper.CheckParameter(newPassword, true, true, false,
                this.Application.MaxPasswordSize, "newPassword");

            // Validate the user before making any changes
            this.ValidateUserWithPassword(user, oldPassword, true);

            // Make sure the new password is ok
            User.ValidatePassword(newPassword);

            // encode the new password
            string encodedPassword = this.EncodePassword(newPassword, 
                user.PasswordFormat, user.PasswordSalt);

            if (encodedPassword.Length > this.Application.MaxPasswordSize)
            {
                throw new ArgumentException("Membership password too long", 
                    "newPassword");
            }

            // Save the new password
            this.PersistChangedPassword(user, encodedPassword);
        }

        #endregion

        #region Private Methods

        private ClientMembershipUser GetClientMembershipUser(string username)
        {
            return this.GetUser(this.GetUser(username));
        }

        private bool CheckPassword(string password, string passwordFromPersistence, 
            PasswordFormat passwordFormat, string salt)
        {
            string encodedPassword = this.EncodePassword(password, 
                passwordFormat, salt);
            return passwordFromPersistence.Equals(encodedPassword);
        }

        private bool CheckPasswordAnswer(string passwordAnswer, 
            string passwordAnswerFromPersistence,
            PasswordFormat passwordFormat, string salt)
        {
            return this.CheckPassword(
                passwordAnswer.ToLower(CultureInfo.InvariantCulture),
                passwordAnswerFromPersistence, passwordFormat, salt);
        }

        private string GetEncodedPasswordAnswer(ClientMembershipUser user, string passwordAnswer)
        {
            if (passwordAnswer != null)
            {
                passwordAnswer = passwordAnswer.Trim();
            }
            if (string.IsNullOrEmpty(passwordAnswer))
            {
                return passwordAnswer;
            }
            return this.EncodePassword(passwordAnswer.ToLower(CultureInfo.InvariantCulture), 
                user.PasswordFormat, user.PasswordSalt);
        }

        private void ThrowHashAlgorithmException()
        {
            throw new Exception("Invalid_hash_algorithm_type");
        }

        private string EncodePassword(string password, 
            PasswordFormat passwordFormat, string salt)
        {
            if (passwordFormat == PasswordFormat.Clear)
            {
                return password;
            }
            byte[] bytes = Encoding.Unicode.GetBytes(password);
            byte[] source = Convert.FromBase64String(salt);
            byte[] destination = new byte[source.Length + bytes.Length];
            byte[] passwordBytes = null;
            Buffer.BlockCopy(source, 0, destination, 0, source.Length);
            Buffer.BlockCopy(bytes, 0, destination, source.Length, bytes.Length);
            if (passwordFormat == PasswordFormat.Hashed)
            {
                HashAlgorithm algorithm = HashAlgorithm.Create(
                    this.Application.HashAlgorithmType);
                if ((algorithm == null) && 
                    this.Application.IsHashAlgorithmFromMembershipConfig)
                {
                    this.ThrowHashAlgorithmException();
                }
                passwordBytes = algorithm.ComputeHash(destination);
            }
            return Convert.ToBase64String(passwordBytes);
        }

        private string GenerateSalt()
        {
            byte[] data = new byte[Constants.SaltSizeInBytes];
            new RNGCryptoServiceProvider().GetBytes(data);
            return Convert.ToBase64String(data);
        }

        private void ValidateUserWithPassword(ClientMembershipUser user,
            string password, bool throwIfFails)
        {
            if (password != null)
            {
                password = password.Trim();
            }

            SecurityHelper.CheckParameter(password,
                true, true, true, 
                this.Application.MaxPasswordAnswerSize,
                "password");

            string passwordFromPersistence =
                this.GetPasswordFromPersistence(user);

            try
            {
                if (!this.CheckPassword(password,
                    passwordFromPersistence,
                    user.PasswordFormat, user.PasswordSalt))
                {
                    user.PasswordAttemptFailed();
                    if (throwIfFails)
                    {
                        throw new SecurityException
                            ("The password supplied was not correct");
                    }
                }
                else
                {
                    user.PasswordAttemptSucceeded();
                }
            }
            finally
            {
                this.PersistUser(user);
            }
        }

        private void ValidateUserWithPasswordAnswer(ClientMembershipUser user,
            string passwordAnswer, bool throwIfFails)
        {
            if (passwordAnswer != null)
            {
                passwordAnswer = passwordAnswer.Trim();
            }

            SecurityHelper.CheckParameter(passwordAnswer,
                this.Application.RequiresQuestionAndAnswer,
                this.Application.RequiresQuestionAndAnswer,
                false, this.Application.MaxPasswordAnswerSize, 
                "passwordAnswer");

            string passwordAnswerFromPersistence =
                this.GetPasswordAnswerFromPersistence(user);

            try
            {
                if (!this.CheckPasswordAnswer(passwordAnswer, 
                    passwordAnswerFromPersistence,
                    user.PasswordFormat, user.PasswordSalt))
                {
                    user.PasswordAnswerAttemptFailed();
                    if (throwIfFails)
                    {
                        throw new SecurityException
                            ("The password answer supplied was not correct");
                    }
                }
                else
                {
                    user.PasswordAnswerAttemptSucceeded();
                }
            }
            finally
            {
                this.PersistUser(user);
            }
        }

        #endregion

        #region IClientMembershipProvider Members

        public abstract User GetUser(object userKey);
        public abstract User GetUser(string username);

        public virtual string GetPassword(string username)
        {
            User.CheckPasswordRetrieval();
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            return this.GetPasswordFromPersistence(user);
        }

        public virtual string GetPassword(string username, string answer)
        {
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            return this.GetPassword(user, answer);
        }

        public virtual bool ChangePassword(string username, string oldPassword, 
            string newPassword)
        {
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            this.ChangePassword(user, oldPassword, newPassword);
            return true;
        }

        public virtual bool ChangePasswordQuestionAndAnswer(string username, 
            string password, string newPasswordQuestion, 
            string newPasswordAnswer)
        {
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            this.ChangePasswordQuestionAndAnswer(user, password, 
                newPasswordQuestion, newPasswordAnswer);
            return true;
        }

        public virtual string ResetPassword(string username)
        {
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            return this.ResetPassword(user);
        }

        public virtual string ResetPassword(string username, string answer)
        {
            ClientMembershipUser user = this.GetClientMembershipUser(username);
            return this.ResetPassword(user, answer);
        }

        public virtual void UpdateUser(User user)
        {
            ClientMembershipUser clientMembershipUser = this.GetUser(user);
            this.unitOfWork.RegisterChanged(clientMembershipUser, this);
            this.unitOfWork.Commit();
        }

        public virtual bool ValidateUser(string username, string password)
        {
            bool validationResult = false;

            // First validate the method parameters
            if (SecurityHelper.ValidateParameter(username, true, true, true,
                this.Application.MaxUsernameSize) && 
                SecurityHelper.ValidateParameter(password, true, true, false,
                this.Application.MaxPasswordSize))
            {
                // Now get the User instance
                ClientMembershipUser user = this.GetClientMembershipUser(username);

                // Validate the user's password
                this.ValidateUserWithPassword(user, password, false);
                validationResult = true;
            }

            // return the result
            return validationResult;
        }

        public abstract Application Application { get; }

        #endregion

        #region IUnitOfWorkRepository Members

        public void PersistNewItem(IEntity item)
        {
            throw new NotImplementedException();
        }

        public void PersistUpdatedItem(IEntity item)
        {
            ClientMembershipUser user = (ClientMembershipUser)item;
            this.PersistUser(user);
        }

        public void PersistDeletedItem(IEntity item)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}
